package il.technion.cs236369.proxy;

import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;

import javax.net.SocketFactory;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.DefaultHttpClientConnection;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.util.EntityUtils;

public class RequestHandler implements HttpRequestHandler, Closeable
{
	private final int ClientPort = 80;
	final int MaxBodyBlobSize = 65535;

	private CacheManager m_cacheManager;
	private SocketFactory m_socketFactory;
	private Socket m_clientSocket;
	
	private DefaultHttpClientConnection m_connection;
	private HttpClient m_client;

	private long m_requestHitCount = 0;
	private long m_requestCount = 0;

	public RequestHandler(SocketFactory socketFactory, CacheManager cacheManager) 
	{
		m_cacheManager = cacheManager;
		m_socketFactory = socketFactory;
		m_clientSocket = null;

		m_connection = null;
		m_client = new HttpClient();
	}

	/**
	 * Compares headers in message to a list of headers
	 * @param message the message to look in
	 * @param headerFilter the header name to filter by
	 * @param headerValues the header values to seek
	 * @return true if found any of the header values
	 */
	private boolean FindHeaderValuesInMessage(HttpMessage message, 
									   String headerFilter,
									   String[] headerValues) 
	{
		// grab all headers that fit the filter
		Header[] messageHeaders = message.getHeaders(headerFilter);
		
		// for every header in the message
		for (Header header : messageHeaders) 
		{
			// get its value
			String messageHeaderValue = header.getValue();
			// and compare to every given header
			for (String headerValue : headerValues)
			{
				if (messageHeaderValue.compareTo(headerValue) == 0)
				{
					return true;
				}
			}
		}
		
		return false;
	}

	/**
	 * Determine whether the message should be cached or not
	 * depending on the cache control headers
	 * @param message the message to check
	 * @return true if message should be cached
	 */
	private boolean ShouldCache(HttpMessage message) 
	{
		if (FindHeaderValuesInMessage(message, HttpHeaders.CACHE_CONTROL,
									  new String[] { "no-store", "no-cache" }))
		{
			return false;
		}
		return true;
	}

	/**
	 * The main handle method
	 */
	@Override
	public void handle(final HttpRequest request,
					   final HttpResponse response,
					   final HttpContext context) 
					   throws HttpException, IOException 
	{
		m_requestCount++;

		if (!request.getRequestLine().getMethod().equalsIgnoreCase("GET")) 
		{
			throw new MethodNotSupportedException("Method not supported");
		}

		if (ShouldCache(request))
		{
			handleRequest(request, response);
		}
	}

	/**
	 * Gets the message's host header
	 * @param message the message to query
	 * @return the host header if available, null if error
	 */
	private String GetMessageHost(HttpMessage message) 
	{
		// get all host headers
		Header[] headers = message.getHeaders(HttpHeaders.HOST);
		
		// if there is only one
		if (headers.length != 1)
		{
			return null;
		}
		
		// return it
		return headers[0].getValue();
	}

	/**
	 * main handling function
	 * 
	 * @param request the request to handle
	 * @param response the response to write to
	 */
	private void handleRequest(HttpRequest request, HttpResponse response)
	{
		try 
		{
			String host = GetMessageHost(request);

			m_clientSocket = m_socketFactory.createSocket(host, ClientPort); 
			m_connection = new DefaultHttpClientConnection();
			
			boolean responseCached = m_cacheManager.ResponseCached(request); 

			if (responseCached) 
			{
				request.setHeader(new BasicHeader(HttpHeaders.IF_NONE_MATCH, 
												  m_cacheManager.GetLatestEntityTag()));
				request.removeHeaders(HttpHeaders.CONNECTION);
				request.setHeader(new BasicHeader(HttpHeaders.CONNECTION, "close"));
			}

			HttpResponse responseReceived = m_client.SendRequest(m_clientSocket, m_connection, request);

			if (responseReceived == null)
			{
				response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
				return;
			}

			if (responseCached)
			{
				if (responseReceived.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED)
				{
					m_cacheManager.WriteCachedResponseBody(response);
					response.setStatusCode(HttpStatus.SC_OK);
					m_requestHitCount++;
					return;
				} 
				else if(responseReceived.getStatusLine().getStatusCode() != HttpStatus.SC_OK) 
				{
					m_cacheManager.DeleteRow(request);
					closeConnections();
					m_clientSocket = m_socketFactory.createSocket(host, ClientPort); 
					m_connection = new DefaultHttpClientConnection();
					request.removeHeaders(HttpHeaders.IF_NONE_MATCH);
					responseReceived = m_client.SendRequest(m_clientSocket, m_connection, request);
					
					if (responseReceived == null)
					{
						response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
						return;
					}
				}
			}

			// Not in cache
			response.setHeaders(responseReceived.getAllHeaders());
			response.setEntity(responseReceived.getEntity());
			response.setStatusLine(responseReceived.getStatusLine());

			if ((responseReceived.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
					|| (ShouldCache(response))
					|| (responseReceived.getHeaders(HttpHeaders.ETAG).length != 0)) 
			{
					HttpEntity entity = response.getEntity();
					
					if (request.getRequestLine().getUri().length() > 255 || 
						entity == null || entity.isChunked() || 
						entity.getContentLength() > MaxBodyBlobSize)
					{
						return;
					}
					
					byte[] body = EntityUtils.toByteArray(response.getEntity());
					response.setEntity(new ByteArrayEntity(body));

					if (body.length > MaxBodyBlobSize)
					{
						return;
					}
					
					m_cacheManager.InsertRow(
							m_cacheManager.CreateEntry(
									request.getRequestLine().getUri(), 
									response.getAllHeaders(), body));
			}
		} 
		catch (Exception e) 
		{
			response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
			System.err.println("Request Handling Error.");
			e.printStackTrace();
		} 
	}

	public long getRequestHitCount() 
	{
		return m_requestHitCount;
	}

	public long getRequestCount() 
	{
		return m_requestCount;
	}
	
	public void closeConnections()
	{
		try
		{ 
			m_clientSocket.close();
		} 
		catch (Exception e)
		{
			e.printStackTrace();
		}

		try 
		{ 
			m_connection.close(); 
		} 
		catch (Exception e) 
		{
			e.printStackTrace();
		}
	}

	@Override
	public void close() throws IOException 
	{
		m_client.CloseConnection();
	}
}
